#include <cstdio>
#include <vector>
#include <string>

#include "SDL.h"
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include "opengl_entry.h"
#include "loaderply.h"

namespace {
const char *g_src_vs =
  "#version 330\n"
  "layout(location = 0) in vec4 in_vertex;"
  "layout(location = 1) in vec3 in_normal;"
  "out vec4 vs_vertex;"
  "out vec3 vs_normal;"
  "uniform mat4 u_mvp;"
  "uniform mat4 u_mv;"
  "void main() {"
    "vs_vertex = u_mv * in_vertex;"
    "vs_normal = mat3(u_mv) * in_normal;"
    "gl_Position = u_mvp * in_vertex;"
  "}";
const char *g_src_fs =
  "#version 330\n"
  "in vec4 vs_vertex;"
  "in vec3 vs_normal;"
  "out vec4 fs_color;"
  "uniform vec3 u_color;"
  "void main() {"
    "vec3 normal_vec = normalize(vs_normal);"
    "vec3 light_vec = normalize(vec3(0, 3, 0) - vs_vertex.xyz);"
    "float n_dot_l = max(dot(normal_vec, light_vec), 0);"
    "fs_color = vec4(0.1) + 0.9 * (vec4(u_color,1) * vec4(n_dot_l));"
  "}";

SDL_WindowID g_window;
SDL_GLContext g_context;
GLuint g_program;
GLuint g_vao;
GLuint g_vertex_buffer;
GLuint g_index_buffer;
GLsizei g_index_count;

bool CreateLighthouse() {
  LoaderPLY loader;
  if (!loader.Load("../../media/lighthouse.ply")) return false;

  g_index_count = loader.index_count();

  glGenBuffers(1, &g_vertex_buffer);
  glBindBuffer(GL_ARRAY_BUFFER, g_vertex_buffer);
  glBufferData(GL_ARRAY_BUFFER, loader.vertex_size() * loader.vertex_count(),
               loader.vertex_data(), GL_STATIC_DRAW);

  glGenVertexArrays(1, &g_vao);
  glBindVertexArray(g_vao);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, loader.vertex_size(), 0);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, loader.vertex_size(),
                        reinterpret_cast<void*>(sizeof(glm::vec3)));
  glEnableVertexAttribArray(0);
  glEnableVertexAttribArray(1);

  glGenBuffers(1, &g_index_buffer);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_index_buffer);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, loader.index_count()*sizeof(glm::uint),
               loader.index_data(), GL_STATIC_DRAW);

  glBindVertexArray(0);

  return true;
}

bool events() {
  SDL_Event evt;
  while (SDL_PollEvent(&evt)) {
    switch (evt.type) {
      case SDL_QUIT:
        return true; // quit
      case SDL_KEYDOWN:
        if (evt.key.keysym.sym == SDLK_ESCAPE) {
          return true; // quit
        }
        break;
    }
  }

  return false;
}

int frame() {
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glUseProgram(g_program);

  glm::mat4 m = glm::lookAt(glm::vec3(2.0f, 3.0f, 2.0f),
                            glm::vec3(0.0f, 0.0f, 0.0f),
                            glm::vec3(0.0f, 1.0f, 0.0f));
  glUniformMatrix4fv(glGetUniformLocation(g_program, "u_mv"), 1, GL_FALSE,
                     glm::value_ptr(m));

  m = glm::perspective(60.0f, 1.333f, 0.1f, 20.0f) * m;
  glUniformMatrix4fv(glGetUniformLocation(g_program, "u_mvp"), 1, GL_FALSE,
                     glm::value_ptr(m));

  glBindVertexArray(g_vao);
  glDrawElements(GL_TRIANGLES, g_index_count, GL_UNSIGNED_INT, 0);

  return 0;
}

int init() {
  // SDL
  if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO) < 0) {
    SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed.");
    return -1;
  }
  SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);

  // window
  SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
  SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
  SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
  SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
  SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
  SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
  SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);

  const int w = 1024, h = 768;
  g_window = SDL_CreateWindow("OpenGL Benchmark 1", SDL_WINDOWPOS_UNDEFINED,
                              SDL_WINDOWPOS_UNDEFINED, w, h, SDL_WINDOW_SHOWN |
                              SDL_WINDOW_OPENGL);
  if (g_window == NULL) {
    SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "SDL_CreateWindow failed.");
    return -1;
  }

  // context
  g_context = SDL_GL_CreateContext(g_window);
  if (g_context == NULL) {
    SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
                    "SDL_GL_CreateContext failed.");
    return -1;
  }
  LoadOpenGLCommands();
  SDL_GL_SetSwapInterval(0);
  
  // program
  g_program = glCreateProgram();

  GLuint sh = glCreateShader(GL_VERTEX_SHADER);
  glAttachShader(g_program, sh);
  glDeleteShader(sh);
  glShaderSource(sh, 1, &g_src_vs, NULL);
  glCompileShader(sh);

  sh = glCreateShader(GL_FRAGMENT_SHADER);
  glAttachShader(g_program, sh);
  glDeleteShader(sh);
  glShaderSource(sh, 1, &g_src_fs, NULL);
  glCompileShader(sh);

  GLint link_status;
  glLinkProgram(g_program);
  glGetProgramiv(g_program, GL_LINK_STATUS, &link_status);
  if (link_status == GL_FALSE) {
    GLchar log[2048];
    glGetProgramInfoLog(g_program, sizeof(log), NULL, log);
    SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
                    "GLSL link error. Info log:\n%s.", log);
    return -1;
  }

  glEnable(GL_DEPTH_TEST);

  return 0;
}

int run() {
  while (true) {
    if (events()) break;
    if (frame() < 0) return -1;
    SDL_GL_SwapWindow(g_window);
  }

  return 0;
}

int quit() {
  if (g_context) {
    glDeleteBuffers(1, &g_vertex_buffer);
    glDeleteBuffers(1, &g_index_buffer);
    glDeleteVertexArrays(1, &g_vao);
    glDeleteProgram(g_program);

    SDL_GL_DeleteContext(g_context);
  }

  SDL_DestroyWindow(g_window);
  SDL_Quit();

  return 0;
}

} // namespace

int main(int argc, char *argv[]) {
  if (init() == 0) {
    run();
  }
  quit();
  return 0;
}
